Skip to content

feat(sdk,slack): webhook sources, agent channels, and human-in-the-loop - #4537

Open
ericallam wants to merge 29 commits into
mainfrom
feat/hosted-webhooks-api
Open

feat(sdk,slack): webhook sources, agent channels, and human-in-the-loop#4537
ericallam wants to merge 29 commits into
mainfrom
feat/hosted-webhooks-api

Conversation

@ericallam

@ericallam ericallam commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

The public SDK and docs half of hosted webhooks: webhook() with typed provider sources (webhooks.stripe(), webhooks.github(), webhooks.svix(), and more, plus webhooks.custom<T>()), chat.event and chat.channels for agent channels, human-in-the-loop tool approvals, the new @trigger.dev/slack connector, and the webhooks docs section.

import { webhook, webhooks } from "@trigger.dev/sdk";

export const stripeWebhook = webhook({
  id: "stripe-webhook",
  source: webhooks.stripe(),
  onEvent: async ({ event, headers, ctx }) => {
    // verified, typed Stripe event
  },
});

Stacked on the server PR

This is the top of a stack. Its base is #4344 (the server half: ingress, delivery pipeline, dashboard, and the shared @trigger.dev/core schemas this SDK builds on), so the diff here is API-only and it builds against a base that already has core.

The single changeset in this PR bumps @trigger.dev/core, @trigger.dev/sdk, @trigger.dev/slack, and trigger.dev together, so core (whose code lands via #4344) is published alongside the SDK.

Merge order

Merges after #4344. The plan: land and deploy the server behind its flag, cut prerelease (rc) packages for early users to test against the live environment, then merge this and cut the real release once the feature is live. When #4344 merges, GitHub retargets this PR's base to main automatically.

@changeset-bot

changeset-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b9797ec

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 30 packages
Name Type
@trigger.dev/core Minor
@trigger.dev/sdk Minor
@trigger.dev/slack Minor
trigger.dev Minor
@trigger.dev/build Minor
@trigger.dev/python Minor
@trigger.dev/redis-worker Minor
@trigger.dev/schema-to-json Minor
@internal/cache Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@internal/metrics-pipeline Patch
@trigger.dev/rbac Minor
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/run-store Patch
@internal/schedule-engine Patch
@trigger.dev/sso Minor
@internal/tracing Patch
@internal/tsql Patch
@internal/webhook-engine Patch
@internal/webhook-sources Patch
@internal/dashboard-agent Patch
@internal/sdk-compat-tests Patch
@trigger.dev/react-hooks Minor
@trigger.dev/rsc Minor
@trigger.dev/database Minor
@trigger.dev/otlp-importer Minor
@internal/testcontainers Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 93ce36fc-9cfa-44e2-a447-8bd239b94bd0

📥 Commits

Reviewing files that changed from the base of the PR and between e75a7ca and f86d3cd.

📒 Files selected for processing (1)
  • packages/trigger-sdk/src/v3/ai.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/trigger-sdk/src/v3/ai.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 8 remain after this review.

📜 Recent review details
⏰ Context from checks skipped due to timeout. (37)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 12)
  • GitHub Check: sdk-compat / Node.js 22.23 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: sdk-compat / Node.js 20.20 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: sdk-compat / Node.js 26.4 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 12)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: sdk-compat / Node.js 24.18 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 12)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-ubuntu-latest-x64-4x - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-ubuntu-latest-x64-4x - npm)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 12)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (1, 3)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 12)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - npm)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 12)
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - pnpm)
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: typecheck / typecheck
  • GitHub Check: internal / 🧪 Unit Tests: Internal
  • GitHub Check: packages / 🧪 Unit Tests: Packages (3, 3)
  • GitHub Check: fk-cascade-guard / fk-cascade-guard
  • GitHub Check: packages / 🧪 Unit Tests: Packages (2, 3)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: audit
  • GitHub Check: audit
  • GitHub Check: 🔍 What moved
  • GitHub Check: code-quality / code-quality
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Build and publish previews

Walkthrough

Added typed webhook sources, provider verification, webhook tasks, and durable chat.event routing. Extended chat agents with channel connectors, streaming replies, reactions, recovery handling, and human-in-the-loop controls. Added the Slack connector package with API operations and tests. Propagated webhook metadata through CLI manifests and added duplicate webhook ID reporting. Added documentation for webhook setup, filtering, deliveries, session routing, channels, and approvals.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the changes and merge context but omits the required issue reference, checklist, testing, changelog, and screenshots sections. Add the template sections, complete the checklist, describe testing steps, add a short changelog entry, include screenshots or state that they are not applicable, and reference the issue.
Docstring Coverage ⚠️ Warning Docstring coverage is 63.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main SDK, Slack, webhook, agent channel, and human-in-the-loop changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hosted-webhooks-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

github-advanced-security[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@ericallam
ericallam force-pushed the feat/hosted-webhooks-api branch from b4b9f89 to 274c3ab Compare August 8, 2026 08:06
@ericallam
ericallam changed the base branch from main to feat/hosted-webhook-ingress August 8, 2026 08:06
@pkg-pr-new

pkg-pr-new Bot commented Aug 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

@trigger.dev/build

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/build@b9797ec

trigger.dev

npm i https://pkg.pr.new/triggerdotdev/trigger.dev@b9797ec

@trigger.dev/core

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/core@b9797ec

@trigger.dev/python

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/python@b9797ec

@trigger.dev/react-hooks

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/react-hooks@b9797ec

@trigger.dev/redis-worker

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/redis-worker@b9797ec

@trigger.dev/rsc

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/rsc@b9797ec

@trigger.dev/schema-to-json

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/schema-to-json@b9797ec

@trigger.dev/slack

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/slack@b9797ec

@trigger.dev/sdk

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/sdk@b9797ec

commit: b9797ec

@ericallam
ericallam force-pushed the feat/hosted-webhooks-api branch 11 times, most recently from 8f0664d to 0a23edf Compare August 9, 2026 18:23
@ericallam
ericallam force-pushed the feat/hosted-webhooks-api branch 4 times, most recently from 38468bb to 2718f32 Compare August 9, 2026 22:26
@ericallam
ericallam force-pushed the feat/hosted-webhooks-api branch 4 times, most recently from 39c0585 to 0debb64 Compare August 11, 2026 05:23
ericallam added a commit that referenced this pull request Aug 16, 2026
#4344)

## Summary

The server half of hosted webhooks: the public ingress endpoint,
signature verification, the delivery pipeline (Postgres partitioned
storage + ClickHouse for ordering), the in-app partition manager, the
HTTP API, and the dashboard (Deliveries, Endpoints, and the in-app test
console).

The public SDK and docs half is #4537. That PR carries the user-facing
API (`webhook()`, `chat.event` / `chat.channels`, the
`@trigger.dev/slack` connector) and builds on the shared
`@trigger.dev/core` schemas that ship here.

## Shipping behind a flag

A `WEBHOOK_ENABLED` env var (default off) gates the public ingress route
and the engine worker plus partition cron, so merging and deploying this
changes nothing in production until it is flipped on per environment.
The dashboard is separately gated per org by the `hasWebhooksAccess`
feature flag.

## Note on packages

This PR includes the `@trigger.dev/core` schema additions the server
compiles against, but carries no changeset. Core is not consumed
independently of the SDK, so it is released together with the SDK via
#4537. Keeping its changeset off `main` means no release cut from `main`
publishes it early.
@ericallam
ericallam force-pushed the feat/hosted-webhooks-api branch from 1399cdb to b3bb085 Compare August 16, 2026 13:33
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

coderabbitai[bot]

This comment was marked as resolved.

…fter one decision

defaultSlackFinalizeInteraction re-rendered the message by dropping every actions
block, so once one tool call was approved or denied the other pending tool calls
in the same message lost their buttons and their turn could not resume. Drop only
the resolved tool call's actions block (matched by the toolCallId:: button value
prefix) and keep the rest.
…rl-secret warning

The typed-event example now passes webhooks.stripe<Stripe.Event>() to webhook()
so it actually demonstrates typed access to event. The verification section no
longer claims every idempotency key comes from a provider event id (it documents
the raw-body/timestamp/signature fallback), and adds a warning that url-secret
exposes the secret in the URL.
…st harness

sendChannelEvent/deliverChannelEvent default connectorId to DEFAULT_TEST_CONNECTOR_ID,
not to a sole configured connector's id as the previous doc implied. Describe the
actual default (which lines up with recordingChannelConnector's own default id).
@ericallam
ericallam marked this pull request as ready for review August 16, 2026 20:58
devin-ai-integration[bot]

This comment was marked as resolved.

…channel interaction

A duplicate or late channel interaction (e.g. a repeated Slack approval
click) whose toolCallId matches no pending tool call was dropped without
detaching the per-turn session.in listener. That listener marks inbound
records CONSUMED, so after the drop the next user message was swallowed
into pendingWireMessages (already drained before the wait) and the turn
idled until the timeout, ending the run and losing the message.

Mirror the action path: detach msgSub and decrement turn (a dropped
interaction is not a turn) before falling through to the wait.
devin-ai-integration[bot]

This comment was marked as resolved.

…l error callbacks

Two channel-connector fixes in the chat.agent turn loop:

- Under a pendingMessages-enabled agent, the mid-turn session.in handler
  returned early for any record without a steerable `.message`. Channel
  deliveries carry `channelEvent` and no `.message`, so an inbound Slack
  message or block_actions approval click arriving mid-turn was neither
  steered nor buffered, and never became a turn. Channel records now fall
  through to pendingWireMessages so they dispatch as the next turn.

- In the turn error handler, resolveReactionChoice (which invokes the
  user's reactions.error callback) and channelConn.outbound ran unguarded.
  A throw escaped the catch, skipping the error chunk, turn-complete, and
  wait-for-next-message, killing the whole conversation. Both are now
  wrapped so a bad user callback degrades to a warn.
devin-ai-integration[bot]

This comment was marked as resolved.

…eMessages agents

A channel interaction (e.g. a Slack approval click) is resolved at the
top of the turn by matching its toolCallId against the in-memory
accumulator. For agents that register hydrateMessages, the accumulator is
empty on a fresh continuation / OOM-retry boot (those agents own
persistence, so the boot seeding block is skipped and the hydrate hook
only runs later in the turn). The click therefore matched nothing, was
classified as a stale interaction, and was dropped, losing the human's
decision and leaving the paused turn unresolved.

When an interaction arrives and hydrateMessages is registered but the
accumulator is empty, load the persisted chain via the hydrate hook first
so the interaction can find its pending tool call. The per-turn hydrate
later in the turn still runs to persist the synthesized resolution
message. Guarded on an empty accumulator so warm turns don't re-hydrate.
devin-ai-integration[bot]

This comment was marked as resolved.

…l error egress

- buildInteractionResolutionMessage matched a tool part by toolCallId
  without checking it was still input-available, so a duplicate or
  retried approval/deny click on an already-answered tool call built a
  fresh resolution and ran the decision a second time. Gate on
  state === 'input-available' so a resolved call resolves to a dropped
  stale interaction, matching the function's documented contract.

- The turn-error channel egress only edited the start-of-turn ack
  placeholder, so a connector configured with ack: null posted nothing on
  a failed turn (the user saw silence). Send a fresh final message when
  there is no placeholder ref. The error text is now run through the
  agent's onError option (same sanitizer the browser stream uses) instead
  of forwarding turnError.message verbatim to the channel surface.
…'t hit a TDZ

The channel-interaction pre-hydrate added for hydrateMessages agents
referenced clientData, but clientData was declared with const later in the
same turn block. The early reference hit the temporal dead zone and threw
ReferenceError, which the local try/catch swallowed, so hydration silently
never ran and the button click was still dropped as a stale interaction.
Parse clientData once above the channel-event block so both the interaction
hydrate and the rest of the turn share the same value.
devin-ai-integration[bot]

This comment was marked as resolved.

…acing, don't clobber posted answers

- channelReplyText selected parts with 'text' in p, which also matches AI
  SDK reasoning parts ({ type: 'reasoning', text }). With sendReasoning
  defaulting to true, a reasoning-capable model's private chain-of-thought
  was concatenated into the text posted to Slack. Narrowed the selection to
  parts whose type is exactly 'text'.

- normalizeKeyString namespaced only the whole placeholder, so a fallback
  key like {a || b} (the Slack connector's DEFAULT_KEY form) left every
  alternative after the first bare, diverging from ValidatedWebhookKey which
  validates each || side and defaults bare sides to the body. It now splits
  on ||, trims, and namespaces each alternative. Added test cases.

- On the turn-error path the error text was sent with previousRef pointing
  at the ack placeholder. If a hook after the final answer was posted threw
  (e.g. onTurnComplete), the delivered answer was edited into the error
  string. Track whether the final answer was posted and, if so, send the
  error as a fresh follow-up instead of editing over the answer.
devin-ai-integration[bot]

This comment was marked as resolved.

…ll is absent; unspace Slack guard in-list

- The channel-interaction pre-hydrate was gated on an empty accumulator, so
  a hydrate-backed agent whose in-memory accumulator was non-empty but no
  longer held the pending tool part (after a chat.history mutation or a
  compaction trim) would skip the hydrate and drop the click as stale. Gate
  instead on the pending toolCallId being absent from the accumulator, so
  the persisted chain is reloaded whenever the call can't be found (a
  duplicate click on an already-resolved call still short-circuits without a
  needless hydrate, since the call is present).

- Unspace the Slack SELF_MESSAGE_GUARD in-list to match the documented
  filter style. The runtime parser already skips whitespace so this is
  cosmetic, but it keeps the guard consistent with the docs example.
devin-ai-integration[bot]

This comment was marked as resolved.

…, skip unroutable channel turns

- webhooks.gitlab used the Svix preset (svix-id/svix-timestamp/svix-signature,
  HMAC over {id}.{timestamp}.{body}), but GitLab doesn't sign bodies: it echoes
  the configured token in X-Gitlab-Token. Every GitLab delivery failed
  verification. Switched it to the shared-secret scheme (header, x-gitlab-token).

- The Slack connector declared secretProvisioning 'integrator', so the Connect
  panel offered a Generate-secret button, but Slack mints its own Signing Secret
  that must be pasted (core already classifies slack as 'provider'). Changed to
  'provider'.

- A verified channel delivery naming a connector the running agent doesn't have
  fell through to a normal turn with an empty incoming message, so the agent
  answered nothing and burned tokens. Handle the unresolved-connector case like a
  stale interaction: warn with the connectorId/deliveryId and skip the turn
  without consuming a turn.
devin-ai-integration[bot]

This comment was marked as resolved.

…ocks

A text-only reply issued chat.update with text alone, so Slack kept whatever
blocks were already on the message (e.g. approval controls previously rendered
into the same ts). Send blocks: [] on edits when the outgoing message has no
blocks, so an edit is authoritative. postMessage still omits blocks when empty.
…ai.ts formatting

The SELF_MESSAGE_GUARD in-list was unspaced earlier to match the documented
filter style, but its unit test still asserted the spaced substring. Update the
assertion. Also apply oxfmt to a wrapped line in the channel error-reaction
block that exceeded the width limit.
devin-ai-integration[bot]

This comment was marked as resolved.

…n't fail a delivered turn

The turn-start 'working' resolver and the success-path 'done' resolver both
awaited user-supplied reactions.working / reactions.done callbacks unguarded. A
rejection propagated into the turn catch, which then posted an error to the
channel and skipped onTurnComplete / the snapshot write, turning an
already-delivered answer into a reported failure. Wrapped both in try/catch that
degrade to a warn, matching the error-path reaction handling.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +204 to +213
/** The verified event body, typed by the source (a preset type, or the `<T>` you supply). */
event: TEvent;
/** The inbound request headers (case-insensitive, Web `Headers`). e.g. headers.get("x-github-event"). */
headers: Headers;
ctx: TaskRunContext;
};

export type WebhookOptions<TIdentifier extends string, TSource extends AnyWebhookSource> = {
id: TIdentifier;
source: TSource;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Generic Slack webhook source can never be connected because it does not answer Slack's URL-verification handshake

The auto-generated Slack source (providerProducers at packages/trigger-sdk/src/v3/webhooks.ts:204-213) is built without the one-time URL-verification handshake or form-payload handling that Slack requires, so a Slack app can never save the endpoint URL.

Impact: Anyone who wires up a plain Slack webhook with the SDK is blocked at setup — Slack rejects the endpoint and no events ever arrive.

Missing handshake/formPayload compared with the Slack connector package

providerProducers is generated from webhookProviderConfigs in packages/core/src/v3/webhooks/index.ts:154, which includes a slack entry. The producer emits verifier: { kind: "config", config: entry.config() } only.

By contrast the dedicated connector in packages/slack/src/index.ts:41-61 builds the same HMAC config but additionally supplies:

  • handshake: SLACK_HANDSHAKE — echoes the url_verification challenge Slack sends when you first save an Event Subscriptions request URL.
  • formPayload: { field: "payload" } on the verifier config — Slack posts interactivity callbacks as application/x-www-form-urlencoded with the JSON in a payload field.

Without the handshake, Slack's "Verify" step fails and the URL cannot be saved. Without formPayload, any interactivity callback body cannot be parsed.

Either exclude slack from the auto-generated producers (pointing users at @trigger.dev/slack), or give the generated producer the same handshake/form-payload configuration.

Prompt for agents
packages/trigger-sdk/src/v3/webhooks.ts generates one producer per entry of webhookProviderConfigs (packages/core/src/v3/webhooks/index.ts). That table contains a `slack` entry, so `webhooks.slack()` is now a public SDK source that only carries the HMAC verifier config. Slack additionally requires (a) answering the one-time `url_verification` handshake before an Event Subscriptions request URL can be saved, and (b) parsing form-encoded interactivity callbacks out of the `payload` field. The dedicated connector in packages/slack/src/index.ts supplies both (SLACK_HANDSHAKE and formPayload on the verifier config), but the generated producer does not, so an endpoint built from `webhooks.slack()` can never be connected. Decide whether to exclude `slack` from the generated producers (and document `@trigger.dev/slack` as the way to receive Slack events) or to attach the handshake + formPayload configuration to the generated Slack producer so it matches the connector.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +337 to +349
/**
* Wait for a channel turn's post-completion egress to land. The run loop
* writes the `trigger:turn-complete` chunk (which unblocks the harness's
* turn-complete latch) BEFORE it awaits the connector's final `send()` and
* its done/error reactions. Draining a handful of macrotasks lets those
* awaited-but-immediate calls settle so `sendChannelEvent` callers can assert
* against the connector's recorded sends/reactions deterministically.
*/
async function settlePostTurnChannelEgress(): Promise<void> {
for (let i = 0; i < 5; i++) {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Channel egress runs after the turn-complete chunk is written, which the test harness has to paper over

writeTurnCompleteChunk is awaited at the end of the turn, and only then is the connector's final send() and the done/error reactions awaited. That ordering means the browser-facing stream closes before the channel reply lands, and any observer keyed on turn-complete (including the test harness) sees the turn as finished with no reply posted yet. mock-chat-agent.ts compensates with settlePostTurnChannelEgress(), a fixed drain of 5 macrotasks, which is inherently timing-based: a connector whose send() does real async work (a fetch, as @trigger.dev/slack does) would not settle within that window, so channel assertions in future tests could be flaky. Consider having the harness await an explicit egress-completed signal instead of draining the microtask/macrotask queue.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants